# -*- coding:utf-8 -*-
__author__ = 'Dragon Sun'
__date__ = '2025-06-22 20:59:37'

from pathlib import Path
from typing import Optional, Callable, Union

import pandas
from PySide6.QtCore import QObject, QSignalBlocker
from PySide6.QtWidgets import QLabel, QListWidget, QRadioButton, QLineEdit, QPushButton, QListWidgetItem, QGroupBox, QCheckBox
from dsOKXv5.data_types import 交易方向枚举
from dsOKXv5.models import 持仓模型类
from dsPyLib.基类.枚举基类 import 枚举基类
from dsPySide6.utils.func import load_ui
from dsPySide6.utils.helper import 设置分组框激活状态, 获取两位小数的数字文本框验证器
from pandas import DataFrame

from src.helpers.helper_ui import 显示错误对话框


class 值守平仓方式枚举(枚举基类):
    MACD段移动止损 = 1
    背离 = 2


FT_吃单 = Callable[[交易方向枚举, float], Union[Optional[持仓模型类], Exception]]  # [[交易方向, 交易数量], 错误|持仓]
FT_市价全平 = Callable[[], Optional[Exception]]  # [[], 错误]
FT_反手 = Callable[[], Union[Optional[持仓模型类]]]  # [[], 错误|持仓]
FT_设置止损 = Callable[[float], Union[Optional[持仓模型类], Exception]]  # [[止损触发价], 错误|持仓]
FT_设置止盈 = Callable[[float], Union[Optional[持仓模型类], Exception]]  # [[止盈触发价], 错误|持仓]
FT_自动值守 = Callable[[bool, set[值守平仓方式枚举]], Optional[Exception]]  # [[自动值守开启状态, 平仓方式], 错误描述]


class 交易视图控制器类(QObject):
    K线最大数量 = 13  # 近期K线列表框中能显示的最大K线数量

    # noinspection PyUnresolvedReferences
    def __init__(self):
        super().__init__()

        # 内部变量
        self._近期K线: Optional[DataFrame] = None
        self._持仓: Optional[持仓模型类] = None
        self._最大下单量: float = 0
        self._自动值守中: bool = False

        # 回调
        self.吃单处理: Optional[FT_吃单] = None
        self.市价全平处理: Optional[FT_市价全平] = None
        self.反手处理: Optional[FT_反手] = None
        self.设置止损处理: Optional[FT_设置止损] = None
        self.设置止盈处理: Optional[FT_设置止盈] = None
        self.自动值守处理: Optional[FT_自动值守] = None

        # 视图
        视图文件名 = Path(__file__).resolve().parent / '交易视图.ui'
        self.视图 = load_ui(ui_file=视图文件名, custom_widgets=[], parent=None)

        # 组件映射
        self._近期K线分组框: QGroupBox = self.视图.gbKLine
        self._近期时间槽列表框: QListWidget = self.视图.lvRecentSlots

        self._开仓方向分组框: QGroupBox = self.视图.gbSide
        self._开多单选框: QRadioButton = self.视图.rbLong
        self._开空单选框: QRadioButton = self.视图.rbShort

        self._K线位置分组框: QGroupBox = self.视图.gbPrice
        self._K线收盘价单选框: QRadioButton = self.视图.rbKClose
        self._K线最值单选框: QRadioButton = self.视图.rbKExtremum
        self._K线柱体最值单选框: QRadioButton = self.视图.rbKBodyExtremum
        self._K线中值单选框: QRadioButton = self.视图.rbKMiddle
        self._K线柱体中值单选框: QRadioButton = self.视图.rbKBodyMiddle
        self._自定义单选框: QRadioButton = self.视图.rbCustom
        self._自定义价格文本框: QLineEdit = self.视图.edtPrice

        self._吃单数量文本框: QLineEdit = self.视图.edtShuliang
        self._吃单全部检查框: QCheckBox = self.视图.cbAll
        self._吃单按钮: QPushButton = self.视图.btnTaker
        self._市价全平按钮: QPushButton = self.视图.btnClosePosition
        self._反手按钮: QPushButton = self.视图.btnFanShou
        self._设置止损按钮: QPushButton = self.视图.btnStopLoss
        self._设置止盈按钮: QPushButton = self.视图.btnTakeProfit
        self._自动值守按钮: QPushButton = self.视图.btnAutoClosePosition
        self._止损移动检查框: QCheckBox = self.视图.cbCloseYiDong
        self._止损背离检查框: QCheckBox = self.视图.cbCloseBeiLi

        self._价格描述标签: QLabel = self.视图.lblSummary

        # 组件事件
        self._近期时间槽列表框.currentItemChanged.connect(self._近期时间槽列表框_选择改变)  # 初始运行会触发第一项

        self._开多单选框.toggled.connect(self._交易方向单选框_选择改变)
        self._开空单选框.toggled.connect(self._交易方向单选框_选择改变)

        self._K线收盘价单选框.toggled.connect(self._K线位置单选框_选择改变)
        self._K线最值单选框.toggled.connect(self._K线位置单选框_选择改变)
        self._K线柱体最值单选框.toggled.connect(self._K线位置单选框_选择改变)
        self._K线中值单选框.toggled.connect(self._K线位置单选框_选择改变)
        self._K线柱体中值单选框.toggled.connect(self._K线位置单选框_选择改变)
        self._自定义单选框.toggled.connect(self._K线位置单选框_选择改变)
        self._自定义价格文本框.textChanged.connect(self._自定义价格文本框_内容改变)

        self._吃单数量文本框.textChanged.connect(self._吃单数量文本框_内容改变)
        self._吃单全部检查框.toggled.connect(self._吃单全部检查框_选择改变)
        self._吃单按钮.clicked.connect(self._吃单按钮_被点击)
        self._市价全平按钮.clicked.connect(self._市价全平按钮_被点击)
        self._反手按钮.clicked.connect(self._反手按钮_被点击)
        self._设置止损按钮.clicked.connect(self._设置止损按钮_被点击)
        self._设置止盈按钮.clicked.connect(self._设置止盈按钮_被点击)
        self._自动值守按钮.clicked.connect(self._自动值守按钮_被点击)

        # 组件初始化显示
        self._组件初始化设置()
        self._更新各组件状态()

    # -------------------- 公共方法 -------------------- #

    def 传递近期K线(self, K线: Optional[DataFrame]):
        """
        定时(每3分钟)将近期(最近13根)的K线数据传递进来
        """
        if K线 is None:
            with QSignalBlocker(self._近期时间槽列表框):  # 避免触发 self._近期时间槽列表框 相关信号
                self._近期时间槽列表框.clear()
            self._更新各组件状态()
            return

        self._近期K线 = K线.tail(self.K线最大数量).copy(deep=True)

        # 更新近期K线显示
        时间槽列表 = self._近期K线['时间槽'].values[::-1]  # 改为降序
        时间槽列表 = [pandas.to_datetime(x).to_pydatetime() for x in 时间槽列表]  # numpy.datetime64 转 datetime
        时间槽列表 = [x.strftime('%Y-%m-%d %H:%M') for x in 时间槽列表]  # datetime 转 格式化字符串

        # 找到原来的选中项在新列表中的索引值(-1代表原来没有选中项或者没有在新列表中)
        索引 = -1
        选中的时间槽 = self._获取选中的时间槽()
        if 选中的时间槽 and (选中的时间槽 in 时间槽列表):
            索引 = 时间槽列表.index(选中的时间槽)

        # 清除所有项/添加新列表项/恢复选中项
        with QSignalBlocker(self._近期时间槽列表框):  # 避免触发 self._近期时间槽列表框 相关信号
            self._近期时间槽列表框.clear()

        if len(时间槽列表):
            self._近期时间槽列表框.addItems(时间槽列表)

            if 索引 == -1:
                self._近期时间槽列表框.setCurrentRow(0)  # 默认选中第一项
            else:
                with QSignalBlocker(self._近期时间槽列表框):  # 避免触发 self._近期时间槽列表框 相关信号
                    self._近期时间槽列表框.setCurrentRow(索引)  # 恢复原来选中项

        self._更新各组件状态()

    def 传递持仓(self, 持仓: Optional[持仓模型类]):
        self._持仓 = 持仓
        self._改变自动值守状态(新状态=self._自动值守中)
        self._更新各组件状态()

    def 传递最大下单量(self, 最大下单量: float):
        self._最大下单量 = 最大下单量
        self._更新各组件状态()

    # -------------------- 组件事件处理 -------------------- #

    def _近期时间槽列表框_选择改变(self, _: QListWidgetItem, __: QListWidgetItem):
        self._价格处理()

    def _交易方向单选框_选择改变(self, checked: bool):
        if checked:
            self._价格处理()

    def _K线位置单选框_选择改变(self, checked: bool):
        if checked:
            self._价格处理()

    def _自定义价格文本框_内容改变(self, _: str):
        if not self._自定义单选框.isChecked():
            self._自定义单选框.setChecked(True)
        self._价格处理()

    def _吃单数量文本框_内容改变(self, _: str):
        self._更新各组件状态()

    def _吃单全部检查框_选择改变(self, checked: bool):
        if checked:
            self._吃单数量文本框.setText('')
        self._更新各组件状态()

    def _吃单按钮_被点击(self, _):
        try:
            if self.吃单处理:
                交易方向 = 交易方向枚举.多 if self._开多单选框.isChecked() else 交易方向枚举.空

                # 处理下单数量
                if self._吃单全部检查框.isChecked():
                    交易数量 = int(self._最大下单量)
                else:
                    交易数量 = float(self._吃单数量文本框.text())
                if (交易数量 == 0) or (交易数量 > self._最大下单量):
                    显示错误对话框(信息=f'当前下单量({交易数量}) > 最大下单量({self._最大下单量})')
                    return

                self.吃单处理(交易方向, 交易数量)
        finally:
            self._吃单数量文本框.setText('')

    def _市价全平按钮_被点击(self, _):
        if self.市价全平处理:
            self.市价全平处理()

    def _反手按钮_被点击(self, _):
        if self.反手处理:
            self.反手处理()

    def _设置止损按钮_被点击(self, _):
        止损触发价 = self._获取价格()
        if 止损触发价 is None:
            显示错误对话框('未指定止损价！')
        else:
            if self.设置止损处理:
                self.设置止损处理(止损触发价)

    def _设置止盈按钮_被点击(self, _):
        止盈触发价 = self._获取价格()
        if 止盈触发价 is None:
            显示错误对话框('未指定止盈价！')
        else:
            if self.设置止盈处理:
                self.设置止盈处理(止盈触发价)

    def _自动值守按钮_被点击(self, _):
        self._改变自动值守状态(新状态=(not self._自动值守中))

    # -------------------- 其它处理 -------------------- #

    def _组件初始化设置(self):
        验证器 = 获取两位小数的数字文本框验证器()  # 让自定义价格文本框只能输入价格(最多6位整数2位小数)
        self._吃单数量文本框.setValidator(验证器)
        self._自定义价格文本框.setValidator(验证器)
        self._近期时间槽列表框.clear()
        self._价格描述标签.setText('')
        self.传递近期K线(K线=None)

    def _更新各组件状态(self):
        有持仓 = self._持仓 is not None
        无持仓 = not 有持仓
        值守中 = 有持仓 and self._自动值守中
        未值守 = not 值守中
        有价格 = self._获取价格() is not None
        有选中K线 = self._获取选中的时间槽() is not None
        已勾选全部 = self._吃单全部检查框.isChecked()
        未勾选全部 = not 已勾选全部
        try:
            吃单数量 = float(self._吃单数量文本框.text())
        except ValueError:
            吃单数量 = 0.0
        指定了有效吃单数量 = (吃单数量 > 0) and 未勾选全部

        设置分组框激活状态(分组框=self._近期K线分组框, 激活状态=(未值守 and 有持仓))
        设置分组框激活状态(分组框=self._开仓方向分组框, 激活状态=(未值守 and 无持仓))
        设置分组框激活状态(分组框=self._K线位置分组框, 激活状态=(未值守 and 有持仓))
        self._K线收盘价单选框.setEnabled(有选中K线)
        self._K线最值单选框.setEnabled(有选中K线)
        self._K线柱体最值单选框.setEnabled(有选中K线)
        self._K线中值单选框.setEnabled(有选中K线)
        self._K线柱体中值单选框.setEnabled(有选中K线)
        self._吃单数量文本框.setEnabled(未值守 and 未勾选全部)
        self._吃单全部检查框.setEnabled(未值守)
        self._吃单按钮.setEnabled(未值守 and (已勾选全部 or 指定了有效吃单数量))
        self._市价全平按钮.setEnabled(未值守 and 有持仓)
        self._反手按钮.setEnabled(未值守 and 有持仓)
        self._设置止损按钮.setEnabled(未值守 and 有持仓 and 有价格)
        self._设置止盈按钮.setEnabled(未值守 and 有持仓 and 有价格)
        self._自动值守按钮.setEnabled(有持仓)
        self._止损移动检查框.setEnabled(未值守 and 有持仓)
        self._止损背离检查框.setEnabled(未值守 and 有持仓)
        self._价格描述标签.setEnabled(未值守 and 有持仓)
        self._价格描述标签.setVisible(未值守 and 有持仓)

        self._吃单数量文本框.setPlaceholderText(f'最大{self._最大下单量:.2f}张')
        self._自动值守按钮.setText('停止自动值守' if 值守中 else '开始自动值守')
        if self._获取选中的时间槽() is None:
            self._自定义单选框.setChecked(True)
        if 有持仓:
            if self._持仓.方向 == 交易方向枚举.多:
                self._开多单选框.setChecked(True)
            else:
                self._开空单选框.setChecked(True)

    # 主要用于显示价格摘要
    def _价格处理(self):
        时间槽选中项 = self._近期时间槽列表框.currentItem()
        选中的交易方向单选框 = self._获取列表中选中的单选框(单选框列表=[
            self._开多单选框,
            self._开空单选框,
        ])
        选中的K线位置单选框 = self._获取列表中选中的单选框(单选框列表=[
            self._K线收盘价单选框,
            self._K线最值单选框,
            self._K线柱体最值单选框,
            self._K线中值单选框,
            self._K线柱体中值单选框,
            self._自定义单选框,
        ])

        if (选中的交易方向单选框 is None) or (选中的K线位置单选框 is None):
            显示错误对话框(信息='选中的交易方向单选框 或 选中的K线位置单选框 不应该为None')
            return

        价格 = self._获取价格()
        交易方向 = 选中的交易方向单选框.text()
        if 时间槽选中项:
            时间槽 = 时间槽选中项.text()
            K线位置 = 选中的K线位置单选框.text().split('(')[0]  # 如果有左括号，则只取左括号前面的部分
            if 选中的K线位置单选框 == self._K线最值单选框:
                细节描述 = '最低价' if 交易方向 == '多' else '最高价'
                K线位置 = f'{K线位置}({细节描述})'
            if 选中的K线位置单选框 == self._K线柱体最值单选框:
                细节描述 = '柱体低值' if 交易方向 == '多' else '柱体高值'
                K线位置 = f'{K线位置}({细节描述})'
            摘要文本 = f'开{交易方向}, K线 {时间槽}, {K线位置}, {价格}'
        else:
            摘要文本 = f'开{交易方向}, 自定义, {价格}'
        self._价格描述标签.setText(摘要文本)
        self._更新各组件状态()

    # 根据所选的K线，开仓方向，K线位置生成对应的价格，如果失败则返回None
    def _获取价格(self) -> Optional[float]:
        # 设置价格的初始值为自定义价格文本框中的内容
        try:
            价格 = float(self._自定义价格文本框.text())
        except ValueError:
            价格 = None

        # 如果没有选中的K线，则意味着价格只能是自定义的
        时间槽项 = self._近期时间槽列表框.currentItem()
        if 时间槽项:
            开多 = self._开多单选框.isChecked()
            时间槽 = 时间槽项.text()
            选中K线 = self._近期K线[self._近期K线['时间槽'] == 时间槽].iloc[0]
            柱体高值 = max(选中K线['开盘价'], 选中K线['收盘价'])
            柱体低值 = min(选中K线['开盘价'], 选中K线['收盘价'])
            柱体中值 = 柱体低值 + (柱体高值 - 柱体低值) / 2
            柱体中值 = round(柱体中值, 2)
            K线中值 = 选中K线['最低价'] + (选中K线['最高价'] - 选中K线['最低价']) / 2
            K线中值 = round(K线中值, 2)

            if self._K线收盘价单选框.isChecked():
                价格 = 选中K线['收盘价']
            elif self._K线最值单选框.isChecked():
                价格 = 选中K线['最低价'] if 开多 else 选中K线['最高价']
            elif self._K线柱体最值单选框.isChecked():
                价格 = 柱体低值 if 开多 else 柱体高值
            elif self._K线中值单选框.isChecked():
                价格 = K线中值
            elif self._K线柱体中值单选框.isChecked():
                价格 = 柱体中值

        return float(价格) if 价格 else 价格

    def _获取选中的时间槽(self) -> Optional[str]:
        选中项 = self._近期时间槽列表框.currentItem()
        return 选中项.text() if 选中项 else None

    @staticmethod
    def _获取列表中选中的单选框(单选框列表: list[QRadioButton]) -> Optional[QRadioButton]:
        for 单选框 in 单选框列表:
            if 单选框.isChecked():
                return 单选框
        return None

    def _改变自动值守状态(self, 新状态: bool):
        老状态 = self._自动值守中
        新状态 = 新状态 if self._持仓 else False  # 只要持仓为None，就取消自动值守
        if (新状态 != 老状态) and self.自动值守处理:
            # 平仓方式
            平仓方式 = set()
            if self._止损移动检查框.isChecked():
                平仓方式.add(值守平仓方式枚举.MACD段移动止损)
            if self._止损背离检查框.isChecked():
                平仓方式.add(值守平仓方式枚举.背离)

            # 调用
            错误 = self.自动值守处理(新状态, 平仓方式)
            if 错误 is None:
                self._自动值守中 = 新状态
                self._更新各组件状态()
